Image Processing with MATLAB
Lecture 35
Overview
$$ % Colors
% Coordinate vectors and matrices
% Common sets
% Abstract vector symbols
% Norms / absolute value
% Optional: dot product spacing (looks nicer in slides)
% Operators $$
Goal
- Today we will treat an image as a matrix (or a stack of matrices).
- We will use MATLAB to apply linear algebra ideas to images:
- linear transformations
- Fourier transform
- singular value decomposition (SVD)
- The emphasis is not on polished code.
- The emphasis is on seeing how our abstract ideas become concrete.
Class plan
- Load an image and inspect it as data
- Manipulate color channels
- Apply linear transformations to the coordinates
- Compute a 2D Fourier transform and filter frequencies
- Compress the image using truncated SVD
Logistics
- If possible, use your own image on your laptop.
- If you do not have one ready, use a built-in MATLAB image such as:
peppers.pngcameraman.tifsaturn.png
- A companion demo script is available in
scripts/image_processing_demo.m. - Working in pairs is fine.
- If MATLAB desktop is not available, MATLAB Online is completely fine.
Part 1: Images as matrices
Warm-up
- A grayscale image is a matrix: \[ A \in \mathbb{R}^{m\times n}. \]
- A color image is usually an \(m\times n\times 3\) array.
- The three slices correspond to:
- red
- green
- blue
- Linear algebra is already built into the data structure.
Step 1: Load an image
clear; close all; clc;
% Option 1: use your own image file
I = imread("my_photo.jpg");
% Option 2: use a built-in sample image
% I = imread("peppers.png");
% I = imread("cameraman.tif");
figure;
imshow(I);
title("Original image");
size(I)
class(I)size(I)tells you the dimensions.- For RGB images, you should see something like
m x n x 3. - For grayscale images, you will see just
m x n.
Step 2: Convert to doubles and grayscale
I = im2double(I); % rescales to numbers between 0 and 1
if size(I,3) == 3
G = rgb2gray(I);
else
G = I;
end
figure;
subplot(1,2,1);
imshow(I);
title("Color image");
subplot(1,2,2);
imshow(G);
title("Grayscale image");- We will use the grayscale image
Gfor Fourier and SVD. - This keeps the linear algebra simpler.
Step 3: Look at the RGB channels
if size(I,3) == 3
R = I(:,:,1);
Gch = I(:,:,2);
B = I(:,:,3);
figure;
subplot(2,2,1); imshow(I); title("Original");
subplot(2,2,2); imshow(R); title("Red channel");
subplot(2,2,3); imshow(Gch); title("Green channel");
subplot(2,2,4); imshow(B); title("Blue channel");
endQuick experiment
J = I;
J(:,:,1) = 0; % remove red
K = I;
K(:,:,3) = 0; % remove blue
figure;
subplot(1,3,1); imshow(I); title("Original");
subplot(1,3,2); imshow(J); title("No red");
subplot(1,3,3); imshow(K); title("No blue");- This is a simple but important point:
- manipulating entries of the array changes the image directly.
Part 2: Linear transformations
Coordinate transformations
- An image lives on a grid of pixel coordinates.
- A linear transformation \[ A = \begin{bmatrix} a & b \\ c & d \end{bmatrix} \] acts on those coordinates.
- Examples:
- rotation
- stretching
- shear
- reflection
- The image values are carried along with the coordinates.
Reading the matrix from its columns
- The first column of \(A\) is \(A\vec{e}_1\).
- The second column of \(A\) is \(A\vec{e}_2\).
- So the columns tell us how the horizontal and vertical directions move.
- Example: \[ A = \begin{bmatrix} 1.15 & 0.35 \\ 0 & 0.85 \end{bmatrix} \] means: \[ A\vec{e}_1 = \langle 1.15,0 \rangle, \qquad A\vec{e}_2 = \langle 0.35,0.85 \rangle. \]
- That already tells us: stretch a bit in the \(x\)-direction and add a shear.
Easy built-in examples
theta = 30; % degrees
I_rot = imrotate(I, theta, "bilinear", "crop");
I_big = imresize(I, 1.3);
I_wide = imresize(I, [size(I,1), round(1.4 * size(I,2))]);
figure;
subplot(2,2,1); imshow(I); title("Original");
subplot(2,2,2); imshow(I_rot); title("Rotated");
subplot(2,2,3); imshow(I_big); title("Scaled up");
subplot(2,2,4); imshow(I_wide); title("Stretched horizontally");- These are convenient, but they hide the linear algebra.
- Next we will specify the matrix ourselves.
Idea before code
- We build the new image one output pixel at a time.
- For each output location \[
q = \langle x_{\text{new}}, y_{\text{new}} \rangle,
\] we ask:
- which point \(p\) in the old image landed here?
- Since the forward rule is \[ q = Ap, \] we solve backwards: \[ p = A^{-1}q. \]
- Then we copy the color of the old pixel nearest to \(p\).
Custom linear map with a matrix
[m,n,channels] = size(I);
cx = (n + 1)/2;
cy = (m + 1)/2;
A = [1.15 0.35;
0.00 0.85]; % stretch + shear
Ainv = inv(A);
J = zeros(size(I));
for row_new = 1:m
for col_new = 1:n
q = [col_new - cx;
row_new - cy];
p = Ainv * q;
col_old = round(p(1) + cx);
row_old = round(p(2) + cy);
if 1 <= row_old && row_old <= m && 1 <= col_old && col_old <= n
for c = 1:channels
J(row_new, col_new, c) = I(row_old, col_old, c);
end
end
end
end
figure;
subplot(1,2,1); imshow(I); title("Original");
subplot(1,2,2); imshow(J); title("Applied matrix A");Why do we use Ainv?
- We are using inverse mapping.
- For each pixel in the output image, we ask:
- where did it come from in the input image?
- This avoids holes and blank gaps.
- In the simple code above, we use
round(...), so each new pixel copies the nearest old pixel.
What each line is doing
cx, cy: move the origin to the center of the imageq: one output pixel written as a centered coordinate vectorp = Ainv*q: trace that output point back to the source imageround(...): choose the nearest source pixelJ(row_new, col_new, :) = I(row_old, col_old, :): copy the color
Try one of these matrices
% Rotation by angle theta
theta = pi/6;
A = [cos(theta) -sin(theta);
sin(theta) cos(theta)];
% Stretch
% A = [1.4 0.0;
% 0.0 0.7];
% Shear
% A = [1.0 0.4;
% 0.0 1.0];
% Reflection across x-axis
% A = [1 0;
% 0 -1];Questions
- Which transformations preserve shape best?
- Which ones preserve area?
- Which ones noticeably change orientation?
- What happens when \(\det(A)=0\) or is very close to \(0\)?
Part 3: Fourier transform
Frequency viewpoint
- The Fourier transform rewrites the image as a sum of waves.
- Low frequencies capture broad structure.
- High frequencies capture rapid changes such as edges, texture, and noise.
- For a grayscale image matrix \(G\), we use \[ F = \operatorname{fft2}(G). \]
What fft2 is doing
- The original image is stored in the standard pixel basis.
- The Fourier transform changes to a different basis:
- horizontal oscillations
- vertical oscillations
- diagonal oscillations
- So each entry of \(F\) tells us:
- which frequency is present
- how strongly it appears
- In other words,
fft2does for images what Fourier series do for 1D signals.
What we read from the spectrum
abs(F)measures the strength of each frequency.angle(F)measures the phase.- For our purposes, magnitude is the easiest thing to visualize first.
- After
fftshift(...), the center of the picture corresponds to low frequencies. - Frequencies farther away from the center correspond to more rapid oscillation.
Visualizing the spectrum
F = fftshift(fft2(G));
S = log(1 + abs(F));
figure;
subplot(1,2,1);
imshow(G, []);
title("Grayscale image");
subplot(1,2,2);
imshow(S, []);
title("Log magnitude of Fourier spectrum");
colormap parula;
colorbar;- The center of the shifted spectrum corresponds to low frequencies.
- Points farther away correspond to higher frequencies.
- We use
log(1 + abs(F))because the raw coefficients can vary a lot in size. - Without the log scale, a few very large coefficients can hide everything else.
Low-pass vs high-pass
- A low-pass filter keeps coefficients near the center.
- So it keeps slowly varying information:
- overall shape
- lighting
- large smooth regions
- A high-pass filter removes that coarse information and keeps rapid changes:
- edges
- texture
- noise
- This is why low-pass filtering blurs, while high-pass filtering highlights outlines.
Low-pass filter
[m,n] = size(G);
[u,v] = meshgrid(-floor(n/2):ceil(n/2)-1, -floor(m/2):ceil(m/2)-1);
R = sqrt(u.^2 + v.^2);
D0 = 40; % cutoff radius
Hlow = double(R <= D0);
Flow = F .* Hlow;
Glow = real(ifft2(ifftshift(Flow)));
figure;
subplot(1,3,1); imshow(G, []); title("Original");
subplot(1,3,2); imshow(Hlow, []); title("Low-pass mask");
subplot(1,3,3); imshow(Glow, []); title("Low-pass result");High-pass filter
Hhigh = double(R >= D0);
Fhigh = F .* Hhigh;
Ghigh = real(ifft2(ifftshift(Fhigh)));
figure;
subplot(1,3,1); imshow(G, []); title("Original");
subplot(1,3,2); imshow(Hhigh, []); title("High-pass mask");
subplot(1,3,3); imshow(mat2gray(Ghigh)); title("High-pass result");- Low-pass filtering tends to blur the image.
- High-pass filtering emphasizes edges and fine details.
Questions
- What visual information is lost under low-pass filtering?
- What kinds of features survive high-pass filtering?
- How does this compare to keeping only a few large singular values?
Part 4: SVD compression
Truncated SVD
- For a grayscale image matrix \(G\), compute \[ G = U\Sigma V^T. \]
- Keeping only the first \(r\) singular values gives \[ G_r = U_r \Sigma_r V_r^T. \]
- This is the best rank-\(r\) approximation in least-squares sense.
How to read the SVD for an image
- Write the SVD as \[ G = \sigma_1 u_1 v_1^T + \sigma_2 u_2 v_2^T + \cdots + \sigma_r u_r v_r^T. \]
- Each term \(\sigma_i u_i v_i^T\) is a rank-one image.
- You can think of it as:
- one vertical pattern \(u_i\)
- one horizontal pattern \(v_i\)
- multiplied together
- The singular value \(\sigma_i\) tells us how important that pattern is.
Why the first few singular values matter
- The singular values are ordered: \[ \sigma_1 \ge \sigma_2 \ge \cdots \ge \sigma_r \ge 0. \]
- So the first few terms capture the dominant large-scale structure.
- Later terms often carry finer detail, small texture, or noise.
- Truncating the SVD means:
- keep the most important patterns
- throw away weaker ones
- That gives compression.
Fourier and SVD are different viewpoints
- Fourier uses a fixed basis of waves.
- SVD uses a data-adaptive basis built from the image itself.
- Fourier asks:
- which frequencies are present?
- SVD asks:
- which rank-one patterns best explain the image?
- Both compress information, but they keep different kinds of structure.
Basic MATLAB code
[U,S,V] = svd(G, "econ");
s = diag(S);
figure;
plot(s / s(1), "LineWidth", 2);
title("Normalized singular values");
xlabel("Index");
ylabel("\sigma_i / \sigma_1");
grid on;Reconstruct with several ranks
ranks = [5 20 50 100];
figure;
subplot(2,3,1);
imshow(G, []);
title("Original");
for j = 1:length(ranks)
r = ranks(j);
Gr = U(:,1:r) * S(1:r,1:r) * V(:,1:r)';
subplot(2,3,j+1);
imshow(Gr, []);
title(sprintf("rank = %d", r));
endEnergy captured
energy = cumsum(s.^2) / sum(s.^2);
figure;
plot(energy, "LineWidth", 2);
xlabel("rank r");
ylabel("cumulative energy");
title("How much of the image is captured?");
grid on;r = 50;
fprintf("Energy captured by rank-%d approximation: %.4f\n", ...
r, energy(r));Why this is called the “best” rank-\(r\) approximation
- Among all matrices of rank at most \(r\), the truncated SVD approximation \[ G_r = U_r \Sigma_r V_r^T \] is the closest to \(G\) in least-squares sense.
- So if we insist on using only \(r\) rank-one pieces, SVD is optimal.
- That is why it is so important in compression and dimension reduction.
Optional: color SVD
R = I(:,:,1); Gch = I(:,:,2); B = I(:,:,3);
r = 40;
[Ur,Sr,Vr] = svd(R, "econ");
[Ug,Sg,Vg] = svd(Gch, "econ");
[Ub,Sb,Vb] = svd(B, "econ");
Rr = Ur(:,1:r) * Sr(1:r,1:r) * Vr(:,1:r)';
Gr = Ug(:,1:r) * Sg(1:r,1:r) * Vg(:,1:r)';
Br = Ub(:,1:r) * Sb(1:r,1:r) * Vb(:,1:r)';
I_svd = cat(3, Rr, Gr, Br);
figure;
subplot(1,2,1); imshow(I); title("Original");
subplot(1,2,2); imshow(I_svd); title("Color SVD approximation");In-class tasks
Core tasks
- Load one image and display its size
- Convert it to grayscale
- Remove one RGB channel or swap two channels
- Apply one custom linear transformation matrix
- Apply one low-pass or high-pass Fourier filter
- Build one rank-\(r\) SVD approximation
Further questions
- Compare two different images: which one needs a larger rank for similar quality?
- Try several cutoff radii in the Fourier filter.
- Compare a low-pass Fourier approximation with a low-rank SVD approximation.
- Try to explain visually:
- what Fourier keeps well
- what SVD keeps well
Some answers
- Fourier vs SVD?
- Fourier decomposes into frequencies.
- SVD decomposes into rank-one patterns adapted to the image.
- Why does low-pass filtering blur the image?
- Because sharp edges need high frequencies, and low-pass removes them.
- Why does high-pass filtering highlight edges?
- Because edges are places where pixel values change rapidly.
- Why does truncated SVD still look recognizable?
- Because the first few singular values often capture most of the important structure.
- Is Fourier better than SVD?
- Not universally. They are useful for different goals and preserve different features.
- Why did we switch to grayscale first?
- It keeps the mathematics cleaner. For color, we can repeat the same idea channel by channel.
Wrap-up
- An image can be understood as:
- a matrix of intensities
- a function on a grid
- a sum of frequency modes
- a sum of rank-one patterns
- This is why image processing is such a natural playground for linear algebra.